Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

23장. 예외 처리

프로그램은 늘 예상대로만 흐르지 않습니다.

파일이 없을 수도 있고,
숫자가 들어와야 할 자리에 글자가 올 수도 있고,
네트워크가 끊길 수도 있습니다.

이렇게 “정상적이지 않은 상황“을
예외(Exception)라고 부릅니다.

이 장에서는 예외를 다루는 기본 문법과,
백엔드에서 오류를 어떻게 설계하면 좋은지 살펴봅니다.


23.1 try, catch, finally

예외가 발생할 수 있는 코드는
try 블록으로 감쌉니다.

예외가 발생하면 catch 블록이 대신 실행됩니다.

fun parseNumber(text: String) {
    try {
        val number = text.toInt()
        println("숫자: $number")
    } catch (e: NumberFormatException) {
        println("숫자로 바꿀 수 없습니다: $text")
    }
}

parseNumber("123")   // 숫자: 123
parseNumber("abc")   // 숫자로 바꿀 수 없습니다: abc

구조를 정리하면 이렇습니다.

  • try : 예외가 생길 수 있는 코드
  • catch : 예외가 생겼을 때 실행할 코드
  • finally : 예외와 상관없이 항상 실행할 코드

finally는 뒷정리에 씁니다.

try {
    println("작업 시작")
    throw RuntimeException("문제 발생")
} catch (e: Exception) {
    println("예외 처리: ${e.message}")
} finally {
    println("항상 실행되는 정리 코드")
}

finally는 예외가 나든 안 나든
반드시 실행됩니다.


23.2 Expression으로서의 try

3장에서 ifwhen
값을 돌려주는 표현식이라고 배웠습니다.

코틀린에서는 try도 표현식입니다.
즉, 결과를 값으로 받을 수 있습니다.

val number = try {
    text.toInt()
} catch (e: NumberFormatException) {
    0   // 실패하면 기본값 0
}

try가 성공하면 그 결과가,
실패하면 catch의 결과가 number에 담깁니다.

이 방식은 코드를 훨씬 간결하게 만들어 줍니다.


23.3 throw

예외를 직접 발생시키려면 throw를 씁니다.

fun checkAge(age: Int) {
    if (age < 0) {
        throw IllegalArgumentException("나이는 음수일 수 없습니다: $age")
    }
    println("나이: $age")
}

여기서 흥미로운 점이 있습니다.
코틀린에서는 throw도 표현식입니다.

그래서 엘비스 연산자(?:)와 함께
자주 쓰입니다.

val name = user.name ?: throw IllegalStateException("이름이 없습니다")

user.name이 null이면
바로 예외를 던지는 코드입니다.
(엘비스 연산자는 5장에서 배웠습니다.)


23.4 Kotlin에는 Checked Exception이 없다

자바를 아는 분이라면
이 부분이 반가울 것입니다.

자바에는 검사 예외(Checked Exception)가 있습니다.
특정 예외는 반드시 try-catch로 처리하거나
throws로 선언해야 했습니다.

// 자바
public void readFile() throws IOException {
    // ...
}

이 규칙은 안전을 위한 것이었지만,
실제로는 의미 없는 try-catch를 양산하기도 했습니다.

코틀린에는 검사 예외가 없습니다.
모든 예외를 강제로 처리하지 않아도 됩니다.

fun readFile() {
    // throws 선언이 필요 없음
}

덕분에 코드가 깔끔해집니다.
다만 그만큼 “어떤 예외를 처리할지“는
개발자가 스스로 판단해야 합니다.


23.5 Custom Exception

기본 예외만으로 부족할 때가 있습니다.
우리 서비스에 맞는 예외를 직접 만들 수 있습니다.

Exception을 상속하면 됩니다.

class UserNotFoundException(
    val userId: Long
) : RuntimeException("회원을 찾을 수 없습니다: $userId")

이렇게 만든 예외는 이렇게 사용합니다.

fun findUser(id: Long): User {
    return userRepository.find(id)
        ?: throw UserNotFoundException(id)
}

직접 만든 예외의 장점은
“무엇이 잘못됐는지“가 이름만으로 드러난다는 점입니다.

UserNotFoundException
RuntimeException보다 훨씬 명확합니다.


23.6 예외 메시지와 오류 정보 설계

예외를 던질 때는
“나중에 이 메시지를 볼 사람“을 생각해야 합니다.

좋은 예외 메시지는 두 가지를 담습니다.

  • 무엇이 잘못됐는가
  • 어떤 값 때문에 잘못됐는가

나쁜 예와 좋은 예를 비교해 봅시다.

// 나쁜 예: 원인을 알 수 없음
throw IllegalArgumentException("잘못된 값")

// 좋은 예: 원인과 값이 드러남
throw IllegalArgumentException("주문 수량은 1 이상이어야 합니다. 입력값: $quantity")

또한 예외에 필요한 정보를
프로퍼티로 담아 두면
나중에 코드로 활용하기 좋습니다.

class PaymentException(
    val orderId: Long,
    val reason: String
) : RuntimeException("결제 실패 [주문 $orderId]: $reason")

예외는 단순히 “터뜨리는 것“이 아니라,
“무슨 일이 있었는지 남기는 기록“입니다.

이렇게 설계해 두면
오류를 추적하고 대응하기가 훨씬 쉬워집니다.


23장을 마치며

이 장에서 우리는 다음을 배웠습니다.

  • try, catch, finally의 기본 구조
  • trythrow가 값을 돌려주는 표현식이라는 점
  • 코틀린에는 검사 예외가 없어 코드가 간결하다는 점
  • 서비스에 맞는 예외를 직접 만드는 방법
  • 원인과 값을 담은 좋은 예외 메시지 설계

예외는 “실패를 표현하는 한 가지 방법“입니다.
다음 장에서는 예외 말고도
실패를 표현하는 여러 방법을 비교해 봅니다.